/*
 * Copyright 2016-2021 Pnoker. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.rundow.common.sdk.service.impl;

import cn.hutool.core.thread.ThreadUtil;
import com.rundow.center.manager.mapper.DriverMapper;
import com.rundow.center.manager.service.DriverPropertyService;
import com.rundow.common.base.bean.driver.AttributeInfo;
import com.rundow.common.base.bean.driver.DriverProperty;
import com.rundow.common.base.bean.driver.DriverRegister;
import com.rundow.common.base.constant.CommonConstant;
import com.rundow.common.base.exception.ServiceException;
import com.rundow.common.base.model.*;
import com.rundow.common.base.model.event.DriverEvent;
import com.rundow.common.base.utils.Dc3Util;
import com.rundow.common.sdk.bean.driver.ContainerContext;
import com.rundow.common.sdk.bean.driver.DriverContext;
import com.rundow.common.sdk.service.DriverMetadataService;
import com.rundow.common.sdk.service.DriverService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;


/**
 * Driver Metadata Service Implements
 *
 * @author pnoker
 */
@Slf4j
@Service
public class DriverMetadataServiceImpl implements DriverMetadataService {

    @Value("${server.port}")
    private int port;

    @Autowired
    private ContainerContext containerContext;

    @Resource
    private DriverService driverService;

    @Resource
    private ThreadPoolExecutor threadPoolExecutor;
    @Resource
    private DriverMapper driverMapper;

    @Autowired
    private DriverPropertyService driverPropertyService;

    @Override
    public void initial() {

        final List<Driver> drivers = driverMapper.selectList(null);
        final List<Driver> driverList = drivers.stream().filter(driver -> driver.getEnable()).collect(Collectors.toList());
        driverList.stream().forEach(driver -> {
            final DriverProperty driverProperty = driverPropertyService.selectAllDriverPropertyById(driver.getId());
            final String driverPropertyName = driverProperty.getName();

            String localHost = Dc3Util.localHost();
            if (!Dc3Util.isName(driverPropertyName) || !Dc3Util.isHost(localHost)) {
                throw new ServiceException("The driver name, service name or host name format is invalid");
            }
            Driver _driver = new Driver(driver.getName(), driverProperty.getType());
            driver.setDescription(driverProperty.getDescription());
            _driver.setId(driver.getId());
            log.info("The driver {} is initializing",  driver.getName());
//            log.info("The driver {}/{} is initializing", driver.getServiceName(), driver.getName());

            registerHandshake(driverPropertyName);
            register(_driver,driverProperty);
            syncDriverMetadata(driverPropertyName);

//            log.info("The driver {}/{} is initialized successfully", driver.getServiceName(), driver.getName());
            log.info("The driver {} is initialized successfully", driver.getName());
        });

    }

    @Override
    public void upsertProfile(String driverName,Profile profile) {
        // Add profile point to context
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        driverContext
                .getDriverMetadata()
                .getProfilePointMap()
                .computeIfAbsent(profile.getId(), k -> new ConcurrentHashMap<>(16));
    }

    @Override
    public void deleteProfile(String driverName,String id) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        driverContext.getDriverMetadata().getProfilePointMap().entrySet().removeIf(next -> next.getKey().equals(id));
    }

    @Override
    public void upsertDevice(String driverName,Device device) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        // Add device to context
        driverContext.getDriverMetadata().getDeviceMap().put(device.getId(), device);
        // Add device driver info to context
        driverContext
                .getDriverMetadata()
                .getDriverInfoMap()
                .computeIfAbsent(device.getId(), k -> new ConcurrentHashMap<>(16));
        // Add device point info to context
        driverContext
                .getDriverMetadata()
                .getPointInfoMap()
                .computeIfAbsent(device.getId(), k -> new ConcurrentHashMap<>(16));
    }

    @Override
    public void deleteDevice(String driverName,String id) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        driverContext.getDriverMetadata().getDeviceMap().entrySet().removeIf(next -> next.getKey().equals(id));
        driverContext.getDriverMetadata().getDriverInfoMap().entrySet().removeIf(next -> next.getKey().equals(id));
        driverContext.getDriverMetadata().getPointInfoMap().entrySet().removeIf(next -> next.getKey().equals(id));
    }

    @Override
    public void upsertPoint(String driverName,Point point) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        // Upsert point to profile point map context
//        driverContext
//                .getDriverMetadata()
//                .getProfilePointMap()
//                .computeIfAbsent(point.getProfileId(), k -> new ConcurrentHashMap<>(16))
//                .put(point.getId(), point);
    }

    @Override
    public void deletePoint(String driverName,String profileId, String pointId) {
        // Delete point from profile point map context
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        driverContext.getDriverMetadata().getProfilePointMap().computeIfPresent(profileId, (k, v) -> {
            v.entrySet().removeIf(next -> next.getKey().equals(pointId));
            return v;
        });
    }

    @Override
    public void upsertDriverInfo(String driverName,DriverInfo driverInfo) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        DriverAttribute attribute = driverContext
                .getDriverMetadata()
                .getDriverAttributeMap()
                .get(driverInfo.getDriverAttributeId());
        if (null != attribute) {
            // Add driver info to driver info map context
//            driverContext
//                    .getDriverMetadata()
//                    .getDriverInfoMap()
//                    .computeIfAbsent(driverInfo.getDeviceId(), k -> new ConcurrentHashMap<>(16))
//                    .put(attribute.getName(), new AttributeInfo(driverInfo.getValue(), attribute.getType()));
        }
    }

    @Override
    public void deleteDriverInfo(String driverName,String deviceId, String attributeId) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        DriverAttribute attribute = driverContext.getDriverMetadata().getDriverAttributeMap().get(attributeId);
        if (null != attribute) {
            // Delete driver info from driver info map context
            driverContext.getDriverMetadata().getDriverInfoMap().computeIfPresent(deviceId, (k, v) -> {
                v.entrySet().removeIf(next -> next.getKey().equals(attribute.getName()));
                return v;
            });

            // If the driver attribute is null, delete the driver info from the driver info map context
            driverContext
                    .getDriverMetadata()
                    .getDriverInfoMap()
                    .entrySet()
                    .removeIf(next -> next.getValue().size() < 1);
        }
    }

    @Override
    public void upsertPointInfo(String driverName,PointInfo pointInfo) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        PointAttribute attribute = driverContext
                .getDriverMetadata()
                .getPointAttributeMap()
                .get(pointInfo.getPointAttributeId());
        if (null != attribute) {
            // Add the point info to the device point info map context
//            driverContext
//                    .getDriverMetadata()
//                    .getPointInfoMap()
//                    .computeIfAbsent(pointInfo.getDeviceId(), k -> new ConcurrentHashMap<>(16))
//                    .computeIfAbsent(pointInfo.getPointId(), k -> new ConcurrentHashMap<>(16))
//                    .put(attribute.getName(), new AttributeInfo(pointInfo.getValue(), attribute.getType()));
        }
    }

    @Override
    public void deletePointInfo(String driverName,String deviceId, String pointId, String attributeId) {
        final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
        PointAttribute attribute = driverContext.getDriverMetadata().getPointAttributeMap().get(attributeId);
        if (null != attribute) {
            // Delete the point info from the device info map context
            driverContext.getDriverMetadata().getPointInfoMap().computeIfPresent(deviceId, (key1, value1) -> {
                value1.computeIfPresent(pointId, (key2, value2) -> {
                    value2.entrySet().removeIf(next -> next.getKey().equals(attribute.getName()));
                    return value2;
                });
                return value1;
            });

            // If the point attribute is null, delete the point info from the point info map context
            driverContext.getDriverMetadata().getPointInfoMap().computeIfPresent(deviceId, (key, value) -> {
                value.entrySet().removeIf(next -> next.getValue().size() < 1);
                return value;
            });
        }
    }

    /**握手事件**/
    private void registerHandshake( final String driverName) {
        try {
            threadPoolExecutor.submit(() -> {
                driverService.driverEventSender(new DriverEvent(this,
                        driverName,
                        CommonConstant.Driver.Event.DRIVER_HANDSHAKE,
                        null
                ));

                final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
                while (!CommonConstant.Status.REGISTERING.equals(driverContext.getDriverStatus())) {
                    ThreadUtil.sleep(500);
                }
            }).get(100, TimeUnit.SECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException exception) {
            exception.printStackTrace();
            driverService.close("The driver initialization failed, Check whether dc3-center-manager are started normally");
        }

    }

    /**注册事件**/
    private void  register(Driver driver, DriverProperty driverProperty){
        driverService.driverEventSender(new DriverEvent(this,
                driver.getName(),
                CommonConstant.Driver.Event.DRIVER_REGISTER,
                new DriverRegister(
                        driverProperty.getTenant(),
                        driver,
                        driverProperty.getDriverAttribute(),
                        driverProperty.getPointAttribute()
                )
        ));

    }

    /**异步发送驱动元数据**/
    private void syncDriverMetadata(final String driverName) {

        try {
            threadPoolExecutor.submit(() -> {
                driverService.driverEventSender(new DriverEvent(this,
                                                                driverName,
                                                                CommonConstant.Driver.Event.DRIVER_METADATA_SYNC,
                                                                driverName
                ));
                final DriverContext driverContext = containerContext.factoryDriverContext(driverName);
                while (!CommonConstant.Status.ONLINE.equals(driverContext.getDriverStatus())) {
                    ThreadUtil.sleep(500);
                }
            }).get(2, TimeUnit.MINUTES);
        } catch (InterruptedException | ExecutionException | TimeoutException exception) {
            exception.printStackTrace();
            driverService.close("The driver initialization failed, Sync driver metadata from dc3-center-manager timeout");
        }

    }
}
